/*
 * Copyright (c) 2022. China Mobile (SuZhou) Software Technology Co.,Ltd. All rights reserved.
 * Lakehouse is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */

package com.chinamobile.cmss.lakehouse.api.service.impl;

import com.chinamobile.cmss.lakehouse.api.service.UserAccessTokenService;
import com.chinamobile.cmss.lakehouse.common.Constants;
import com.chinamobile.cmss.lakehouse.common.enums.Status;
import com.chinamobile.cmss.lakehouse.common.utils.DateUtils;
import com.chinamobile.cmss.lakehouse.common.utils.EncryptionUtils;
import com.chinamobile.cmss.lakehouse.common.utils.ParameterUtils;
import com.chinamobile.cmss.lakehouse.common.utils.Result;
import com.chinamobile.cmss.lakehouse.dao.UserAccessTokenDao;
import com.chinamobile.cmss.lakehouse.dao.UserDao;
import com.chinamobile.cmss.lakehouse.dao.entity.UserAccessTokenEntity;
import com.chinamobile.cmss.lakehouse.dao.entity.UserEntity;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import javax.persistence.criteria.Predicate;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class UserAccessTokenServiceImpl extends BaseServiceImpl implements UserAccessTokenService {

    @Autowired
    private UserAccessTokenDao userAccessTokenDao;

    @Autowired
    private UserDao userDao;

    /**
     * query user access token list paging
     *
     * @param loginUser login user
     * @param pageNo    page number
     * @param searchVal search value
     * @param pageSize  page size
     * @return token list page
     */
    @Override
    public Result queryUserAccessTokenList(UserEntity loginUser, String searchVal, Integer pageNo, Integer pageSize) {
        Result<Object> result = new Result<>();
        PageRequest pageRequest = PageRequest.of(pageNo - 1, pageSize, Sort.Direction.DESC, "updateTime");
        Specification<UserAccessTokenEntity> specification = buildSpecification(loginUser, searchVal);
        Page<UserAccessTokenEntity> pageInfo = userAccessTokenDao.findAll(specification, pageRequest);
        result.setData(pageInfo);
        putMessage(result, Status.SUCCESS);
        return result;
    }

    /**
     * query user access token
     *
     * @param loginUser login user
     * @param userId    user id
     * @return token list
     */
    @Override
    public Map<String, Object> queryUserAccessTokenByUser(UserEntity loginUser, String userId) {
        Map<String, Object> result = new HashMap<>();
        result.put(Constants.STATUS, false);

        // only admin can operate
        if (isNotAdmin(loginUser, result)) {
            return result;
        }

        // query access token for specified user
        List<UserAccessTokenEntity> accessTokenList = userAccessTokenDao.findByUserId(userId);
        result.put(Constants.DATA_LIST, accessTokenList);
        putMessage(result, Status.SUCCESS);
        return result;
    }

    /**
     * create access token
     *
     * @param loginUser  login user
     * @param userId     user id
     * @param expireTime expire time
     * @param token      token
     * @return create access token
     */
    @Override
    public Map<String, Object> createAccessToken(UserEntity loginUser, String userId, String expireTime, String token) {
        Map<String, Object> result = new HashMap<>();

        //check if user is existed
        UserEntity userEntity = userDao.findByUserName(userId);
        if (null == userEntity) {
            putMessage(result, Status.USER_NOT_EXIST, userId);
            return result;
        }

        //check permission
        if (!isAllowed(loginUser, userEntity.getUserId())) {
            putMessage(result, Status.USER_NO_OPERATION_PERM);
            return result;
        }

        //generate access token if absent
        if (StringUtils.isBlank(token)) {
            token = EncryptionUtils.getMd5(userId + expireTime + System.currentTimeMillis());
        }

        //persist to the database
        UserAccessTokenEntity accessToken = UserAccessTokenEntity.builder()
            .userId(userId)
            .expireTime(DateUtils.stringToDate(expireTime))
            .token(token).build();

        userAccessTokenDao.save(accessToken);
        result.put(Constants.DATA_LIST, accessToken);
        putMessage(result, Status.SUCCESS);
        return result;
    }

    /**
     * generate token
     *
     * @param loginUser  login user
     * @param userId     user id
     * @param expireTime expire time
     * @return token
     */
    @Override
    public Map<String, Object> generateAccessToken(UserEntity loginUser, String userId, String expireTime) {
        Map<String, Object> result = new HashMap<>();
        //check if user is existed
        UserEntity userEntity = userDao.findByUserName(userId);
        if (null == userEntity) {
            putMessage(result, Status.USER_NOT_EXIST, userId);
            return result;
        }
        if (!isAllowed(loginUser, userEntity.getUserId())) {
            putMessage(result, Status.USER_NO_OPERATION_PERM);
            return result;
        }
        String token = EncryptionUtils.getMd5(userId + expireTime + System.currentTimeMillis());
        result.put(Constants.DATA_LIST, token);
        putMessage(result, Status.SUCCESS);
        return result;
    }

    /**
     * delete user access token by id
     *
     * @param loginUser login user
     * @param id        token id
     * @return delete token result
     */
    @Override
    public Map<String, Object> deleteUserAccessTokenById(UserEntity loginUser, int id) {
        Map<String, Object> result = new HashMap<>();

        Optional<UserAccessTokenEntity> optional = userAccessTokenDao.findById(id);

        if (!optional.isPresent()) {
            log.error("access token not exist,  access token id {}", id);
            putMessage(result, Status.ACCESS_TOKEN_NOT_EXIST);
            return result;
        }

        UserEntity userEntity = userDao.findByUserName(optional.get().getUserId());
        if (null == userEntity || !isAllowed(loginUser, userEntity.getUserId())) {
            putMessage(result, Status.USER_NO_OPERATION_PERM);
            return result;
        }

        userAccessTokenDao.deleteById(id);
        putMessage(result, Status.SUCCESS);
        return result;
    }

    /**
     * update user access token
     *
     * @param loginUser  login user
     * @param id         token id
     * @param userId     user id
     * @param expireTime expire time
     * @param token      token
     * @return updated user access token entity
     */
    @Override
    public Map<String, Object> updateUserAccessToken(UserEntity loginUser, int id, String userId, String expireTime, String token) {
        Map<String, Object> result = new HashMap<>();
        //check if user is existed
        UserEntity userEntity = userDao.findByUserName(userId);
        if (null == userEntity) {
            putMessage(result, Status.USER_NOT_EXIST, userId);
            return result;
        }

        // check permission
        if (!isAllowed(loginUser, userEntity.getUserId())) {
            putMessage(result, Status.USER_NO_OPERATION_PERM);
            return result;
        }

        // check if token is existed
        Optional<UserAccessTokenEntity> optional = userAccessTokenDao.findById(id);
        if (!optional.isPresent()) {
            log.error("access token not exist,  access token id {}", id);
            putMessage(result, Status.ACCESS_TOKEN_NOT_EXIST);
            return result;
        }

        //generate access token if absent
        if (StringUtils.isBlank(token)) {
            token = EncryptionUtils.getMd5(userId + expireTime + System.currentTimeMillis());
        }

        // persist to the database
        UserAccessTokenEntity accessToken = optional.get().toBuilder()
            .userId(userId)
            .expireTime(DateUtils.stringToDate(expireTime))
            .token(token).build();

        userAccessTokenDao.save(accessToken);

        result.put(Constants.DATA_LIST, accessToken);
        putMessage(result, Status.SUCCESS);
        return result;
    }

    private Specification<UserAccessTokenEntity> buildSpecification(UserEntity loginUser, String searchVal) {
        return (root, criteriaQuery, cBuilder) -> {
            //define a predicate
            Predicate p = cBuilder.conjunction();
            if (isAdmin(loginUser)) {
                if (StringUtils.isNotBlank(searchVal)) {
                    p = cBuilder.and(p, cBuilder.like(root.get("userId"), "%" + ParameterUtils.replaceSpecialChars(searchVal) + "%"));
                }
            } else {
                p = cBuilder.and(p, cBuilder.equal(root.get("userId"), loginUser.getUserName()));
            }
            return p;
        };
    }
}
